package org.hashdb.client.net.nio;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import jline.console.UserInterruptException;
import jline.console.completer.Completer;
import jline.console.completer.StringsCompleter;
import lombok.Getter;
import org.hashdb.client.cmd.CommandExecutorDispatcher;
import org.hashdb.client.config.AppConfig;
import org.hashdb.client.console.AppConsoleReader;
import org.hashdb.client.console.decorator.StringDecorator;
import org.hashdb.client.manager.NioAppContext;
import org.hashdb.client.net.nio.msg.*;
import org.hashdb.client.util.AsyncService;
import org.hashdb.client.util.JsonService;
import org.jetbrains.annotations.Nullable;

import java.io.Closeable;
import java.io.IOException;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.TimeoutException;

/**
 * Date: 2024/1/19 3:06
 *
 * @author huanyuMake-pecdle
 * @version 0.0.1
 */
public class Client implements Closeable {

    private static final AppConfig config = AppConfig.getInstance();

    private Long id;

    private Channel channel;

    private HashChannelHandler channelHandler;

    private ServerAddress serverAddress;

    private final List<String> usedHost = new LinkedList<>();

    private final List<Integer> usedPort = new LinkedList<>();
    private final EventLoopGroup group = new NioEventLoopGroup();

    private final AppConsoleReader reader;

    private final NioAppContext appContext;

    /**
     * 与服务器交流所使用的协议
     */
    private Protocol protocol = Protocol.HASH_V1;
    @Getter
    private String user;

    private volatile boolean closed = false;

    private class ServerAddress {
        String host = config.getHost();
        int port = config.getPort();

        @Override
        public String toString() {
            return host + ":" + port;
        }

        public void save() {
            usedHost.add(host);
            usedPort.add(port);
        }
    }

    public Client(NioAppContext ctx) {
        this.reader = ctx.reader();
        this.appContext = ctx;
        bindListener(ctx.dispatcher());
    }

    public void setUser(String username) {
        this.user = username;
        appContext.publish("setUser", username);
    }

    public Protocol protocol() {
        return protocol;
    }

    public void setProtocol(Protocol protocol) {
        var req = new ProtocolSwitchingMessage(protocol.key());
        pausePublishFormReader();
        channelHandler.newRequest(req).apply().handleAsync((res, e) -> {
            if (e != null) {
                return handleRequestError(e, null);
            }
            if (res instanceof ErrorMessage errorMessage) {
                reader.errorLn("%s : %s", errorMessage.body().exception(), errorMessage.body().cause());
                return tryPublishFormReader();
            }
            if (res instanceof ActMessage<?> act) {
                var body = act.body();
                if (body != null) {
                    reader.messageLn(body.toString());
                    this.protocol = protocol;
                    appContext.publish("setProtocol", protocol);
                    return tryPublishFormReader();
                }
            }
            reader.messageLn("unexpected msg: " + res);
            return tryPublishFormReader();
        }, AsyncService.service());
    }

    CompletableFuture<Boolean> reconnectSession() {
        if (channel == null || !channel.isActive() || id == null) {
            return CompletableFuture.completedFuture(false);
        }
        System.out.println("reconnectSession");
        var req = new ReconnectMessage(id);
        pausePublishFormReader();
        return channelHandler.newRequest(req).apply().handleAsync((res, e) -> {
            if (e != null) {
                return handleRequestError(e, false);
            }
            if (res instanceof ErrorMessage errorMessage) {
                reader.errorLn("%s : %s", errorMessage.body().exception(), errorMessage.body().cause());
                return tryPublishFormReader(false);
            }
            Object body = res.body();
            if (body == null) {
                channel.writeAndFlush(new ErrorMessage(res.id(), "response message body is null"));
                return tryPublishFormReader(false);
            }
            reader.messageLn(body.toString());
            return tryPublishFormReader(true);
        }, AsyncService.service());
    }

    CompletableFuture<Boolean> reconnectServer(String host, int port) {
        if (closed) {
            throw new IllegalStateException();
        }
        var future = new CompletableFuture<Boolean>();
        reconnectSession().thenAcceptAsync(ok -> {
            if (ok) {
                future.complete(true);
                return;
            }
            if (channel != null) {
                try {
                    channel.disconnect().sync();
                } catch (InterruptedException e) {
                    reader.errorLn(e.getClass().getSimpleName() + " : " + e.getMessage());
                }
                channel.close();
            }
            reader.message("\nreconnecting to " + host + ":" + port + " ");
            ChannelFuture channelFuture;
            try {
                channelHandler = new HashChannelHandler(appContext);
                channelFuture = new Bootstrap().group(group)
                        .channel(NioSocketChannel.class)
                        .handler(channelHandler)
                        .connect(host, port);
            } catch (Exception e) {
                reader.println(StringDecorator.red("FAILED"));
                reader.errorLn("cause: " + StringDecorator.red(e.getMessage()));
                future.complete(false);
                return;
            }
            channel = channelFuture.channel();
            channelFuture.addListener(r -> {
                reader.println(StringDecorator.green("CONNECTED"));
                future.complete(true);
            });
        }, AsyncService.service());
        return future;
    }


    private <R> R handleRequestError(Throwable e, R result) {
        if (e instanceof CompletionException c) {
            return handleRequestError(c.getCause(), result);
        }
        String old = reader.getErrorPrefix();
        reader.setErrorPrefix(StringDecorator.red("[REQUEST ERROR] "));
        if (e instanceof TimeoutException) {
            reader.errorLn("TIMEOUT");
        } else {
            reader.errorLn("%s : %s", e.getClass().getSimpleName(), e.getMessage());
            reader.setErrorPrefix(old);
        }
        return tryPublishFormReader(result);
    }

    private CompletableFuture<?> tryConnect(ServerAddress serverAddress) {
        var future = new CompletableFuture<>();
        reconnectServer(serverAddress.host, serverAddress.port).thenAcceptAsync(ok -> {
            if (ok) {
                future.complete(null);
                return;
            }
            reader.messageLn("please set host and port again:");
            try {
                serverAddress.host = reader.read("[HOST] >")
                        .notEmpty()
                        .addCompleter(usedHostCompleter())
                        .check(ipLike -> {
                            try {
                                // 尝试解析为IPV4
                                var inetAddress = InetAddress.getByName(ipLike);
                                if (inetAddress.isReachable(5000)) {
                                    return true;
                                }
                                throw new IOException();
                            } catch (UnknownHostException e) {
                                // 如果不是有效的IPv4地址，尝试解析为IPv6
                                try {
                                    var inetAddress = InetAddress.getByAddress(ipLike, Inet6Address.getByName(ipLike).getAddress());
                                    if (inetAddress instanceof Inet6Address) {
                                        return true;
                                    }
                                    throw new UnknownHostException();
                                } catch (UnknownHostException ex) {
                                    reader.errorLn("unknown host '" + ipLike + "'");
                                    return false;
                                }
                            } catch (IOException e) {
                                reader.errorLn("can not connect to server '" + ipLike + "'");
                                return false;
                            }
                        })
                        .exit()
                        .apply();
                reader.read("[PORT] >")
                        .notEmpty()
                        .addCompleter(usedHostCompleter())
                        .check(input -> {
                            try {
                                serverAddress.port = Integer.parseInt(input);
                            } catch (NumberFormatException ex) {
                                reader.errorLn("require an integer but get '" + input + "'");
                                return false;
                            }
                            if (serverAddress.port >= 0 && serverAddress.port <= 65535) {
                                return true;
                            }
                            reader.errorLn("port out of range [0,65535]");
                            return false;
                        })
                        .exit()
                        .apply();
            } catch (IOException e) {
                reader.printStackTrace(e);
            } finally {
                tryConnect(serverAddress).thenRunAsync(() -> future.complete(null), AsyncService.service());
            }
        }, AsyncService.service());
        return future;
    }

    // TODO: 2024/1/22 这里应该是有bug的, 应该要让用户自主选择断开当前连接, 而不是一直在断线后, 重连这个
    // 第一次可以默认连接, 但是后面就不行了, 后面断线后, 让用户自己输入ip, port来连接,
    // 可以在这里给AppConsoleReader多加一个Completer来提示可以输入的IP, 比如可以输入默认IP与PORT
    // 以及以往输入的ip与port
    public CompletableFuture<?> tryConnect() {
        pausePublishFormReader();
        var serverAddress = new ServerAddress();
        return tryConnect(serverAddress).thenRunAsync(() -> {
            this.serverAddress = serverAddress;
            tryPublishFormReader();
        }, AsyncService.service());
    }

    @Override
    public void close() {
        if (closed) {
            return;
        }
        if (channel != null && channel.isActive()) {
            channel.close();
        }
        try {
            group.shutdownGracefully().sync();
        } catch (InterruptedException e) {
            reader.printStackTrace(e);
        }
        closed = true;
    }

    Channel channel() {
        if (channel == null || closed) {
            throw new IllegalStateException();
        }
        return channel;
    }

    private void bindListener(CommandExecutorDispatcher dispatcher) {
        dispatcher.listen("connect", "CONNECT", "reconnect", "RECONNECT")
                .register(m -> tryConnect())
                .listen("exit", "EXIT")
                .register(m -> {
                    if (reader.ok("[EXIT] confirm?")) {
                        System.exit(0);
                    }
                })
                // 切换协议
                .listen("prot", "PROT", "protocol", "PROTOCOL")
                .register(m -> {
                    reader.completerManager().onProtocol();
                    try {
                        reader.read("PROTOCOL")
                                .notEmpty()
                                .check(p -> {
                                    var protocolLike = p.toUpperCase();
                                    Protocol protocol;
                                    try {
                                        protocol = Protocol.valueOf(protocolLike);
                                    } catch (IllegalArgumentException e) {
                                        reader.errorLn("unsupported protocol '%s'", protocolLike);
                                        return false;
                                    }
                                    setProtocol(protocol);
                                    reader.completerManager().offProtocol();
                                    return true;
                                }).apply();
                    } catch (IOException e) {
                        reader.printStackTrace(e);
                    }
                })
                .listen("auth", "AUTH")
                .register(m -> {
                    String username;
                    String password;
                    reader.completerManager().offClient();
                    try {
                        username = reader
                                .read()
                                .addBuffer("hash")
                                .notEmpty()
                                .prompt("[USERNAME] >")
                                .newPrompt(s -> StringDecorator.red("[USERNAME] >"))
                                .apply();
                        password = reader.read("[PASSWORD] >").password().apply();
                    } catch (UserInterruptException e) {
                        reader.completerManager().onClient();
                        return;
                    } catch (IOException e) {
                        reader.completerManager().onClient();
                        reader.printStackTrace(e);
                        return;
                    }
                    reader.completerManager().onClient();
                    authWithPassword(username, password);
                })
                .register("", m -> {
                })
                .registerNotFound(notFountMessage -> {
                    if (!(notFountMessage.eventKey() instanceof String command)) {
                        dispatcher.getDefaultNotFoundListener().accept(notFountMessage);
                        return;
                    }
                    if (channel == null || !channel.isActive()) {
                        reader.errorLn("no server connected. please run 'RECONNECT'");
                        return;
                    }
                    var commandMessage = new AppCommandMessage(command);
                    pausePublishFormReader();
                    channelHandler.newRequest(commandMessage).apply().handleAsync((res, e) -> {
                        if (e != null) {
                            return handleRequestError(e, null);
                        }
                        if (res instanceof ErrorMessage errorMessage) {
                            printErrorMsg(errorMessage);
                            return tryPublishFormReader();
                        }
                        if (res instanceof ActAppCommandMessage actAppCommandMessage) {
                            printActCommandMsg(actAppCommandMessage);
                            return tryPublishFormReader();
                        }
                        reader.errorLn("unexpected msg: " + res);
                        return tryPublishFormReader();
                    }, AsyncService.service());
                });
    }

    private void pausePublishFormReader() {
        appContext.dispatcher().pausePublishFormReader();
    }

    private Void tryPublishFormReader() {
        return tryPublishFormReader(null);
    }

    private <R> R tryPublishFormReader(@Nullable R result) {
        appContext.dispatcher().tryPublishFormReader();
        return result;
    }

    private void authWithPassword(String username, String password) {
        var req = new AuthenticationMessage(username, password);
        pausePublishFormReader();
        channelHandler.newRequest(req).apply().handleAsync((res, e) -> {
            if (e != null) {
                return handleRequestError(e, null);
            }
            if (res instanceof ErrorMessage errorMessage) {
                printErrorMsg(errorMessage);
                return tryPublishFormReader();
            }
            if (res instanceof ActAuthenticationMessage act) {
                reader.message("auth pass");
                setUser(act.body().username());
                return tryPublishFormReader();
            }
            reader.errorLn("unexpected msg: " + res);
            return tryPublishFormReader();
        }, AsyncService.service());
    }

    private void printErrorMsg(ErrorMessage msg) {
        reader.errorLn(msg.body().exception() + ": " + msg.body().cause());
    }

    private void printActCommandMsg(ActAppCommandMessage act) {
        reader.println(StringDecorator.green("[RESULT] --------------"));
        reader.println(JsonService.prettyStringfy(act.body()));
    }

    public DisconnectHandler disconnectHandler() {
        return new DisconnectHandler();
    }

    public SessionStateHandler sessionStateHandler() {
        return new SessionStateHandler();
    }

    class DisconnectHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
            if (cause instanceof SocketException) {
                reader.errorLn("disconnected to server '%s'", serverAddress);
                serverAddress = null;
                return;
            }
            reader.errorLn(cause.getClass().getSimpleName() + " : " + cause.getMessage());
        }
    }

    class SessionStateHandler extends ChannelInboundHandlerAdapter {
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            if (!(msg instanceof SessionStateMessage sessionStateMessage)) {
                ctx.fireChannelRead(msg);
                return;
            }
            var state = sessionStateMessage.body();
            if (state == null) {
                ctx.writeAndFlush(new ErrorMessage(sessionStateMessage.id(), "session state is null"));
                return;
            }
            Client.this.id = state.id();
        }
    }

    public void disconnect() {
        if (channel == null) {
            return;
        }
        channel.close();
        channel = null;
        serverAddress = null;
    }

    private Completer usedHostCompleter() {
        return new StringsCompleter(usedHost.toArray(String[]::new));
    }

    private Completer usedPortCompleter() {
        return new StringsCompleter(usedPort.stream().map(String::valueOf).toArray(String[]::new));
    }
}